
using System.Linq;

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;

using PS.DataTypes;
using PS.Engine.DataTypes;
using PS.Engine.GameComponents;
using PS.Algorithms;
using PS.Engine.Helper;
using System.Collections.Generic;
using System;
using System.Collections;
using PS.Stereotypes;

namespace PS.Engine
{
    /// <summary>
    /// This is the main type for your game
    /// </summary>
    public class PedestrianSimulation : Game
    {
        #region Const

        //private const string SerializationDocumentPath = @"..\..\..\env_simpleColDet.xml";
        private const string SerializationDocumentPath = @"..\..\..\PS.Engine\PS.Engine\env_prototype1.xml";
        //private const string SerializationDocumentPath = @"..\..\..\env_prototype2.xml";
        //private const string SerializationDocumentPath = @"..\..\..\env_prototype3.xml";

        #endregion

        private readonly GraphicsDeviceManager graphics;
        private SpriteBatch spriteBatch;
        private World world;
        private SimulationInfo simulationInfo;
        private bool paused;
        private double actualElapsedTime;
        private bool displayWaypoints = true;
        
        public delegate void SimulationEventHandler(object sender, SimulationEventArgs e);
        public event SimulationEventHandler SimulationChanged;

        internal World World
        {
            get
            {
                return this.world;
            }
        }

        public PedestrianSimulation(bool displayWaypoints)
            : this(SerializationDocumentPath, displayWaypoints)
        {
        }

        public PedestrianSimulation(string serializationDocumentPath, bool displayWaypoints)
        {
            graphics = new GraphicsDeviceManager(this);
            this.displayWaypoints = displayWaypoints;
            Content.RootDirectory = "Content";

            // init simulation info
            simulationInfo = new SimulationInfo();
            simulationInfo.TimeElapsed = 0;
            simulationInfo.SimulationRunning = true;

            // load the environment from the xml definition
            world = new World();
            world.LoadEnvironment(serializationDocumentPath, this);
            UpdateComponents(world);

            // set initial simulation information
            simulationInfo.PedestrianCount = world.Pedestrians.Count;
            simulationInfo.PedestrianHomeCount = 0;

            // Get the waypoint list for the pedestrian => shortest path
            Vector2[] waypointArray = (from w in world.Waypoints select new Vector2(w.Location.X, w.Location.Y)).ToArray();

            foreach (Pedestrian p in world.Pedestrians)
            {
                Vertex[] shortestPath = Dijkstra.GetShortestPath(new Graph(world.Vertices, world.Edges), p.StartWaypoint.CorrespondingVertex, p.TargetWaypoint.CorrespondingVertex);

                if (shortestPath == null || shortestPath.Length == 0) break; // No path for current user

                WaypointList shortestPathList = new WaypointList();
                for (int i = 0; i < shortestPath.Length; i++)
                {
                    shortestPathList.Enqueue(world.Waypoints.ElementAt(shortestPath[i].Id));
                }

                p.Waypoints = shortestPathList;
            }

            CalculateInitialSimulationInformation();
        }

        #region Lifecycle

        public void Pause()
        {
            paused = true;
        }

        public void Unpause()
        {
            paused = false;
        }

        /// <summary>
        /// Allows the game to perform any initialization it needs to before starting to run.
        /// This is where it can query for any required services and load any non-graphic
        /// related content.  Calling base.Initialize will enumerate through any components
        /// and initialize them as well.
        /// </summary>
        protected override void Initialize()
        {
            base.Initialize();

            Viewport viewport = graphics.GraphicsDevice.Viewport;
        }

        /// <summary>
        /// LoadContent will be called once per game and is the place to load
        /// all of your content.
        /// </summary>
        protected override void LoadContent()
        {
            spriteBatch = new SpriteBatch(graphics.GraphicsDevice);
        }

        /// <summary>
        /// Allows the game to run logic such as updating the world,
        /// checking for collisions, gathering input, and playing audio.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Update(GameTime gameTime)
        {
            if (!paused)
            {
                actualElapsedTime += gameTime.ElapsedGameTime.TotalMilliseconds;
                base.Update(gameTime);   
            }
            // update the simulation info with current values
            UpdateSimulationInfo(actualElapsedTime);
            if (SimulationChanged != null)
            {
                SimulationChanged(this, new SimulationEventArgs(simulationInfo));
            }
        }

        private void UpdateSimulationInfo(double actualElapsedTime)
        {
            // set time elapsed
            simulationInfo.TimeElapsed = (int)actualElapsedTime;
            
            // get currently home count
            int pedestriansCurrentlyAtHome = world.Pedestrians.Where(p => p.AtGoal).Count();
            
            // check if first one just arrived
            if (simulationInfo.PedestrianHomeCount == 0 && pedestriansCurrentlyAtHome > 0)
            {
                // calculate min time to goal
                simulationInfo.PedestriansMinTime = world.Pedestrians.Where(p => p.AtGoal).Min(p => p.TimeToGoal);

                // calculate min actual distance to goal
                simulationInfo.PedestriansMinDistance = world.Pedestrians.Where(p => p.AtGoal).Min(p => p.ActualDistanceToGoal);
            }

            // set new home count
            simulationInfo.PedestrianHomeCount = pedestriansCurrentlyAtHome;

            // set min speed
            simulationInfo.MinSpeed = world.Pedestrians.Min(p => p.MinSpeed);
            simulationInfo.MaxSpeed = world.Pedestrians.Max(p => p.MaxSpeed);
            
            // check if last man standing finally arrives
            if (simulationInfo.SimulationRunning)
            {
                if (simulationInfo.PedestrianCount == simulationInfo.PedestrianHomeCount)
                {
                    // set simulation info to not running
                    simulationInfo.SimulationRunning = false;

                    // calculate max time to goal
                    simulationInfo.PedestriansMaxTime = world.Pedestrians.Max(p => p.TimeToGoal);

                    // calculate max actual distance to goal
                    simulationInfo.PedestriansMaxDistance = world.Pedestrians.Where(p => p.AtGoal).Max(p => p.ActualDistanceToGoal);

                    // calculate min boor businessman time
                    simulationInfo.BoorBusinessmanTime = world.Pedestrians.Where(p => p.Stereotypes.Any(s => s.GetType() == typeof(Boor))).Where(p => p.Stereotypes.Any(s => s.GetType() == typeof(Businessman))).Min(p => p.TimeToGoal);
                    
                    // calculate max time for our lovely gentle grandma
                    simulationInfo.GentlemanLikeGrandmaTime = world.Pedestrians.Where(p => p.Stereotypes.Any(s => s.GetType() == typeof(Gentleman))).Where(p => p.Stereotypes.Any(s => s.GetType() == typeof(Grandma))).Min(p => p.TimeToGoal);
                }

                // calculate averages if already someone at home
                if (pedestriansCurrentlyAtHome > 0)
                {
                    // get average time to goal
                    simulationInfo.PedestriansAvgTime = world.Pedestrians.Where(p => p.AtGoal).Average(p => p.TimeToGoal);

                    // get average actual distance to goal
                    simulationInfo.PedestriansAvgDistance = world.Pedestrians.Where(p => p.AtGoal).Average(p => p.ActualDistanceToGoal);
                }
            }
        }

        private void CalculateInitialSimulationInformation()
        {
            // calculate initial distance for all pedestrians
            foreach (Pedestrian p in world.Pedestrians)
            {
                p.CalculateInitialDistance();
            }

            // get min initial distance
            simulationInfo.PedestriansMinInitialDistance = world.Pedestrians.Min(p => p.InitialDistanceToGoal);

            // get max initial distance
            simulationInfo.PedestriansMaxInitialDistance = world.Pedestrians.Max(p => p.InitialDistanceToGoal);

            // get avg initial distance
            simulationInfo.PedestriansAvgInitialDistance = world.Pedestrians.Average(p => p.InitialDistanceToGoal);

        }

        /// <summary>
        /// This is called when the game should draw itself.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.White);

            base.Draw(gameTime);
        }

        #endregion

        #region private Methods

        private void UpdateComponents(World world)
        {
            // Obstacles
            foreach (Obstacle o in world.Obstacles)
            {
                Components.Add(o);
            }

            // Waypoints
            if (displayWaypoints)
            {
                foreach (Waypoint w in world.Waypoints)
                {
                    Components.Add(w);
                }
            }

            // Pedestrians
            foreach (Pedestrian p in world.Pedestrians)
            {
                Components.Add(p);
            }
        }

        #endregion
    }
}
